<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
 <head>
  <meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
  <meta content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no" name="viewport"/>
  <meta content="zh-cn" http-equiv="content-language"/>
  <meta content="16 Spring Exception 常见错误" name="description"/>
  <link href="/static/favicon.png" rel="icon"/>
  <title>
   16 Spring Exception 常见错误
  </title>
  <link href="/static/index.css" rel="stylesheet"/>
  <link href="/static/highlight.min.css" rel="stylesheet"/>
  <script src="/static/highlight.min.js">
  </script>
  <meta content="Hexo 4.2.0" name="generator"/>
  <script async="" data-website-id="83e5d5db-9d06-40e3-b780-cbae722fdf8c" defer="" src="https://analyze.lianglianglee.com/umami.js">
  </script>
 </head>
 <body>
  <div class="book-container">
   <div class="book-sidebar">
    <div class="book-brand">
     <a href="/">
      <img src="/static/favicon.png"/>
      <span>
       技术文章摘抄
      </span>
     </a>
    </div>
    <div class="book-menu uncollapsible">
     <ul class="uncollapsible">
      <li>
       <a class="current-tab" href="/">
        首页
       </a>
      </li>
      <li>
       <a href="../">
        上一级
       </a>
      </li>
     </ul>
     <ul class="uncollapsible">
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/00%20%e5%af%bc%e8%af%bb%205%e5%88%86%e9%92%9f%e8%bd%bb%e6%9d%be%e4%ba%86%e8%a7%a3Spring%e5%9f%ba%e7%a1%80%e7%9f%a5%e8%af%86.html" id="00 导读 5分钟轻松了解Spring基础知识.md">
        00 导读 5分钟轻松了解Spring基础知识.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/00%20%e5%bc%80%e7%af%87%e8%af%8d%20%e8%b4%b4%e5%bf%83%e2%80%9c%e4%bf%9d%e5%a7%86%e2%80%9dSpring%e7%bd%a2%e5%b7%a5%e4%ba%86%e6%80%8e%e4%b9%88%e5%8a%9e%ef%bc%9f.html" id="00 开篇词 贴心“保姆”Spring罢工了怎么办？.md">
        00 开篇词 贴心“保姆”Spring罢工了怎么办？.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/01%20Spring%20Bean%20%e5%ae%9a%e4%b9%89%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af.html" id="01 Spring Bean 定义常见错误.md">
        01 Spring Bean 定义常见错误.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/02%20Spring%20Bean%20%e4%be%9d%e8%b5%96%e6%b3%a8%e5%85%a5%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af%ef%bc%88%e4%b8%8a%ef%bc%89.html" id="02 Spring Bean 依赖注入常见错误（上）.md">
        02 Spring Bean 依赖注入常见错误（上）.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/03%20Spring%20Bean%20%e4%be%9d%e8%b5%96%e6%b3%a8%e5%85%a5%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af%ef%bc%88%e4%b8%8b%ef%bc%89.html" id="03 Spring Bean 依赖注入常见错误（下）.md">
        03 Spring Bean 依赖注入常见错误（下）.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/04%20Spring%20Bean%20%e7%94%9f%e5%91%bd%e5%91%a8%e6%9c%9f%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af.html" id="04 Spring Bean 生命周期常见错误.md">
        04 Spring Bean 生命周期常见错误.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/05%20Spring%20AOP%20%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af%ef%bc%88%e4%b8%8a%ef%bc%89.html" id="05 Spring AOP 常见错误（上）.md">
        05 Spring AOP 常见错误（上）.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/06%20Spring%20AOP%20%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af%ef%bc%88%e4%b8%8b%ef%bc%89.html" id="06 Spring AOP 常见错误（下）.md">
        06 Spring AOP 常见错误（下）.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/07%20Spring%e4%ba%8b%e4%bb%b6%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af.html" id="07 Spring事件常见错误.md">
        07 Spring事件常见错误.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/08%20%e7%ad%94%e7%96%91%e7%8e%b0%e5%9c%ba%ef%bc%9aSpring%20Core%20%e7%af%87%e6%80%9d%e8%80%83%e9%a2%98%e5%90%88%e9%9b%86.html" id="08 答疑现场：Spring Core 篇思考题合集.md">
        08 答疑现场：Spring Core 篇思考题合集.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/09%20Spring%20Web%20URL%20%e8%a7%a3%e6%9e%90%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af.html" id="09 Spring Web URL 解析常见错误.md">
        09 Spring Web URL 解析常见错误.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/10%20Spring%20Web%20Header%20%e8%a7%a3%e6%9e%90%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af.html" id="10 Spring Web Header 解析常见错误.md">
        10 Spring Web Header 解析常见错误.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/11%20Spring%20Web%20Body%20%e8%bd%ac%e5%8c%96%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af.html" id="11 Spring Web Body 转化常见错误.md">
        11 Spring Web Body 转化常见错误.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/12%20Spring%20Web%20%e5%8f%82%e6%95%b0%e9%aa%8c%e8%af%81%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af.html" id="12 Spring Web 参数验证常见错误.md">
        12 Spring Web 参数验证常见错误.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/13%20Spring%20Web%20%e8%bf%87%e6%bb%a4%e5%99%a8%e4%bd%bf%e7%94%a8%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af%ef%bc%88%e4%b8%8a%ef%bc%89.html" id="13 Spring Web 过滤器使用常见错误（上）.md">
        13 Spring Web 过滤器使用常见错误（上）.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/14%20Spring%20Web%20%e8%bf%87%e6%bb%a4%e5%99%a8%e4%bd%bf%e7%94%a8%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af%ef%bc%88%e4%b8%8b%ef%bc%89.html" id="14 Spring Web 过滤器使用常见错误（下）.md">
        14 Spring Web 过滤器使用常见错误（下）.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/15%20Spring%20Security%20%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af.html" id="15 Spring Security 常见错误.md">
        15 Spring Security 常见错误.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/16%20Spring%20Exception%20%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af.html" id="16 Spring Exception 常见错误.md">
        16 Spring Exception 常见错误.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/17%20%e7%ad%94%e7%96%91%e7%8e%b0%e5%9c%ba%ef%bc%9aSpring%20Web%20%e7%af%87%e6%80%9d%e8%80%83%e9%a2%98%e5%90%88%e9%9b%86.html" id="17 答疑现场：Spring Web 篇思考题合集.md">
        17 答疑现场：Spring Web 篇思考题合集.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/18%20Spring%20Data%20%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af.html" id="18 Spring Data 常见错误.md">
        18 Spring Data 常见错误.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/19%20Spring%20%e4%ba%8b%e5%8a%a1%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af%ef%bc%88%e4%b8%8a%ef%bc%89.html" id="19 Spring 事务常见错误（上）.md">
        19 Spring 事务常见错误（上）.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/20%20Spring%20%e4%ba%8b%e5%8a%a1%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af%ef%bc%88%e4%b8%8b%ef%bc%89.html" id="20 Spring 事务常见错误（下）.md">
        20 Spring 事务常见错误（下）.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/21%20Spring%20Rest%20Template%20%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af.html" id="21 Spring Rest Template 常见错误.md">
        21 Spring Rest Template 常见错误.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/22%20Spring%20Test%20%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af.html" id="22 Spring Test 常见错误.md">
        22 Spring Test 常见错误.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/23%20%e7%ad%94%e7%96%91%e7%8e%b0%e5%9c%ba%ef%bc%9aSpring%20%e8%a1%a5%e5%85%85%e7%af%87%e6%80%9d%e8%80%83%e9%a2%98%e5%90%88%e9%9b%86.html" id="23 答疑现场：Spring 补充篇思考题合集.md">
        23 答疑现场：Spring 补充篇思考题合集.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/%e5%af%bc%e8%af%bb%205%e5%88%86%e9%92%9f%e8%bd%bb%e6%9d%be%e4%ba%86%e8%a7%a3%e4%b8%80%e4%b8%aaHTTP%e8%af%b7%e6%b1%82%e7%9a%84%e5%a4%84%e7%90%86%e8%bf%87%e7%a8%8b.html" id="导读 5分钟轻松了解一个HTTP请求的处理过程.md">
        导读 5分钟轻松了解一个HTTP请求的处理过程.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/%e7%9f%a5%e8%af%86%e5%9b%9e%e9%a1%be%20%e7%b3%bb%e7%bb%9f%e6%a2%b3%e7%90%86Spring%e7%bc%96%e7%a8%8b%e9%94%99%e8%af%af%e6%a0%b9%e6%ba%90.html" id="知识回顾 系统梳理Spring编程错误根源.md">
        知识回顾 系统梳理Spring编程错误根源.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/%e7%bb%93%e6%9d%9f%e8%af%ad%20%e9%97%ae%e9%a2%98%e6%80%bb%e6%af%94%e8%a7%a3%e5%86%b3%e5%8a%9e%e6%b3%95%e5%a4%9a.html" id="结束语 问题总比解决办法多.md">
        结束语 问题总比解决办法多.md
       </a>
      </li>
      <li>
       <a href="/assets/捐赠.md">
        捐赠
       </a>
      </li>
     </ul>
    </div>
   </div>
   <div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseleave="remove_inner()" onmouseover="add_inner()">
    <div class="sidebar-toggle-inner">
    </div>
   </div>
   <div class="off-canvas-content">
    <div class="columns">
     <div class="column col-12 col-lg-12">
      <div class="book-navbar">
       <header class="navbar">
        <section class="navbar-section">
         <a onclick="open_sidebar()">
          <i class="icon icon-menu">
          </i>
         </a>
        </section>
       </header>
      </div>
      <div class="book-content" style="max-width: 960px; margin: 0 auto;
    overflow-x: auto;
    overflow-y: hidden;">
       <div class="book-post">
        <div align="center">
         <a href="https://www.aliyun.com/minisite/goods?userCode=lc4iupk4" target="_blank">
          阿里云2C2G3M 99元/年，老用户也可以哦
         </a>
         <hr/>
        </div>
        <p align="center" id="tip">
        </p>
        <p class="title">
         16 Spring Exception 常见错误
        </p>
        <div>
         <p>
          你好，我是傅健。
         </p>
         <p>
          今天，我们来学习 Spring 的异常处理机制。Spring 提供了一套健全的异常处理框架，以便我们在开发应用的时候对异常进行处理。但是，我们也会在使用的时候遇到一些麻烦，接下来我将通过两个典型的错误案例，带着你结合源码进行深入了解。
         </p>
         <h2 id="案例-1-小心过滤器异常">
          案例 1：小心过滤器异常
         </h2>
         <p>
          为了方便讲解，我们还是沿用之前在事务处理中用到的学生注册的案例，来讨论异常处理的问题：
         </p>
         <pre><code>@Controller
@Slf4j
public class StudentController {
    public StudentController(){
        System.out.println("construct");
    }


    @PostMapping("/regStudent/{name}")
    @ResponseBody
    public String saveUser(String name) throws Exception {
        System.out.println("......用户注册成功");
        return "success";
    }
}
</code></pre>
         <p>
          为了保证安全，这里需要给请求加一个保护，通过验证 Token 的方式来验证请求的合法性。这个 Token 需要在每次发送请求的时候带在请求的 header 中，header 的 key 是 Token。
         </p>
         <p>
          为了校验这个 Token，我们引入了一个 Filter 来处理这个校验工作，这里我使用了一个最简单的 Token：111111。
         </p>
         <p>
          当 Token 校验失败时，就会抛出一个自定义的 NotAllowException，交由 Spring 处理：
         </p>
         <pre><code>@WebFilter
@Component
public class PermissionFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader("token");


        if (!"111111".equals(token)) {
            System.out.println("throw NotAllowException");
            throw new NotAllowException();
        }
        chain.doFilter(request, response);
    }


    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }


    @Override
    public void destroy() {
    }
</code></pre>
         <p>
          NotAllowException 就是一个简单的 RuntimeException 的子类：
         </p>
         <pre><code>public class NotAllowException extends RuntimeException {
    public NotAllowException() {
        super();
    }
}
</code></pre>
         <p>
          同时，新增了一个 RestControllerAdvice 来处理这个异常，处理方式也很简单，就是返回一个 403 的 resultCode：
         </p>
         <pre><code>@RestControllerAdvice
public class NotAllowExceptionHandler {
    @ExceptionHandler(NotAllowException.class)
    @ResponseBody
    public String handle() {
        System.out.println("403");
        return "{\"resultCode\": 403}";
    }
}
</code></pre>
         <p>
          为了验证一下失败的情况，我们模拟了一个请求，在 HTTP 请求头里加上一个 Token，值为 111，这样就会引发错误了，我们可以看看会不会被 NotAllowExceptionHandler 处理掉。
         </p>
         <p>
          然而，在控制台上，我们只看到了下面这样的输出，这其实就说明了 NotAllowExceptionHandler 并没有生效。
         </p>
         <pre><code>throw NotAllowException
</code></pre>
         <p>
          想下问题出在哪呢？我们不妨对 Spring 的异常处理过程先做一个了解。
         </p>
         <h3 id="案例解析">
          案例解析
         </h3>
         <p>
          我们先来回顾一下
          <a href="https://time.geekbang.org/column/article/376115" target="_blank">
           第13课
          </a>
          讲过的过滤器执行流程图，这里我细化了一下：
         </p>
         <p>
          <img alt="" src="assets/26d2a5c3443f46f299add44843e1d70f.jpg"/>
         </p>
         <p>
          从这张图中可以看出，当所有的过滤器被执行完毕以后，Spring 才会进入 Servlet 相关的处理，而 DispatcherServlet 才是整个 Servlet 处理的核心，它是前端控制器设计模式的实现，提供 Spring Web MVC 的集中访问点并负责职责的分派。正是在这里，Spring 处理了请求和处理器之间的对应关系，以及这个案例我们所关注的问题——统一异常处理。
         </p>
         <p>
          其实说到这里，我们已经了解到过滤器内异常无法被统一处理的大致原因，就是因为异常处理发生在上图的红色区域，即DispatcherServlet中的doDispatch()，而此时，过滤器已经全部执行完毕了。
         </p>
         <p>
          下面我们将深入分析 Spring Web 对异常统一处理的逻辑，深刻理解其内部原理。
         </p>
         <p>
          <strong>
           首先我们来了解下ControllerAdvice是如何被Spring加载并对外暴露的。
          </strong>
          在Spring Web 的核心配置类 WebMvcConfigurationSupport 中，被 @Bean 修饰的 handlerExceptionResolver()，会调用addDefaultHandlerExceptionResolvers() 来添加默认的异常解析器。
         </p>
         <pre><code>@Bean
public HandlerExceptionResolver handlerExceptionResolver(
      @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager) {
   List&lt;HandlerExceptionResolver&gt; exceptionResolvers = new ArrayList&lt;&gt;();
   configureHandlerExceptionResolvers(exceptionResolvers);
   if (exceptionResolvers.isEmpty()) {
      addDefaultHandlerExceptionResolvers(exceptionResolvers, contentNegotiationManager);
   }
   extendHandlerExceptionResolvers(exceptionResolvers);
   HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();
   composite.setOrder(0);
   composite.setExceptionResolvers(exceptionResolvers);
   return composite;
}
</code></pre>
         <p>
          最终按照下图的调用栈，Spring 实例化了ExceptionHandlerExceptionResolver类。
         </p>
         <p>
          <img alt="" src="assets/f24b8e58e19947309a1e41d35d1b6568.jpg"/>
         </p>
         <p>
          从源码中我们可以看出，ExceptionHandlerExceptionResolver 类实现了InitializingBean接口，并覆写了afterPropertiesSet()。
         </p>
         <pre><code>public void afterPropertiesSet() {
   // Do this first, it may add ResponseBodyAdvice beans
   initExceptionHandlerAdviceCache();
    //省略非关键代码
}
</code></pre>
         <p>
          并在 initExceptionHandlerAdviceCache() 中完成了所有 ControllerAdvice 中的ExceptionHandler 的初始化。其具体操作，就是查找所有 @ControllerAdvice 注解的 Bean，把它们放到成员变量 exceptionHandlerAdviceCache 中。
         </p>
         <p>
          在我们这个案例里，就是指 NotAllowExceptionHandler 这个异常处理器。
         </p>
         <pre><code>private void initExceptionHandlerAdviceCache() {
   //省略非关键代码
   List&lt;ControllerAdviceBean&gt; adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
   for (ControllerAdviceBean adviceBean : adviceBeans) {
      Class&lt;?&gt; beanType = adviceBean.getBeanType();
      if (beanType == null) {
         throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
      }
      ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
      if (resolver.hasExceptionMappings()) {
         this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
      }
 //省略非关键代码
}
</code></pre>
         <p>
          到这，我们可以总结一下，WebMvcConfigurationSupport 中的handlerExceptionResolver() 实例化并注册了一个ExceptionHandlerExceptionResolver 的实例，而所有被 @ControllerAdvice 注解修饰的异常处理器，都会在 ExceptionHandlerExceptionResolver 实例化的时候自动扫描并装载在其类成员变量 exceptionHandlerAdviceCache 中。
         </p>
         <p>
          当第一次请求发生时，DispatcherServlet 中的 initHandlerExceptionResolvers() 将获取所有注册到 Spring 的 HandlerExceptionResolver 类型的实例，而ExceptionHandlerExceptionResolver 恰好实现了 HandlerExceptionResolver 接口，这些 HandlerExceptionResolver 类型的实例则会被写入到类成员变量handlerExceptionResolvers中。
         </p>
         <pre><code>private void initHandlerExceptionResolvers(ApplicationContext context) {
   this.handlerExceptionResolvers = null;

   if (this.detectAllHandlerExceptionResolvers) {
      // Find all HandlerExceptionResolvers in the ApplicationContext, including ancestor contexts.
      Map&lt;String, HandlerExceptionResolver&gt; matchingBeans = BeanFactoryUtils
            .beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
      if (!matchingBeans.isEmpty()) {
         this.handlerExceptionResolvers = new ArrayList&lt;&gt;(matchingBeans.values());
         // We keep HandlerExceptionResolvers in sorted order.
         AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
      }
      //省略非关键代码
}

</code></pre>
         <p>
          <strong>
           接着我们再来了解下ControllerAdvice是如何被Spring消费并处理异常的。
          </strong>
          下文贴出的是核心类 DispatcherServlet 中的核心方法 doDispatch() 的部分代码：
         </p>
         <pre><code>protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
   //省略非关键代码

   try {
      ModelAndView mv = null;
      Exception dispatchException = null;
      try {
         //省略非关键代码
         //查找当前请求对应的 handler，并执行
         //省略非关键代码
      }
      catch (Exception ex) {
         dispatchException = ex;
      }
      catch (Throwable err) {
         dispatchException = new NestedServletException("Handler dispatch failed", err);
      }
      processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
   }
   //省略非关键代码
</code></pre>
         <p>
          Spring 在执行用户请求时，当在“查找”和“执行”请求对应的 handler 过程中发生异常，就会把异常赋值给 dispatchException，再交给 processDispatchResult() 进行处理。
         </p>
         <pre><code>private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
      @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
      @Nullable Exception exception) throws Exception {
   boolean errorView = false;
   if (exception != null) {
      if (exception instanceof ModelAndViewDefiningException) {
         mv = ((ModelAndViewDefiningException) exception).getModelAndView();
      }
      else {
         Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
         mv = processHandlerException(request, response, handler, exception);
         errorView = (mv != null);
      }
   }
   //省略非关键代码
</code></pre>
         <p>
          进一步处理后，即当 Exception 不为 null 时，继续交给 processHandlerException处理。
         </p>
         <pre><code>protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
      @Nullable Object handler, Exception ex) throws Exception {
   //省略非关键代码
   ModelAndView exMv = null;
   if (this.handlerExceptionResolvers != null) {
      for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
         exMv = resolver.resolveException(request, response, handler, ex);
         if (exMv != null) {
            break;
         }
      }
   }
   //省略非关键代码
}
</code></pre>
         <p>
          然后，processHandlerException 会从类成员变量 handlerExceptionResolvers 中获取有效的异常解析器，对异常进行解析。
         </p>
         <p>
          显然，这里的 handlerExceptionResolvers 一定包含我们声明的NotAllowExceptionHandler#NotAllowException 的异常处理器的 ExceptionHandlerExceptionResolver 包装类。
         </p>
         <h3 id="问题修正">
          问题修正
         </h3>
         <p>
          为了利用 Spring MVC 的异常处理机制，我们需要对 Filter 做一些改造。手动捕获异常，并将异常 HandlerExceptionResolver 进行解析处理。
         </p>
         <p>
          我们可以这样修改 PermissionFilter，注入 HandlerExceptionResolver：
         </p>
         <pre><code>@Autowired
@Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
</code></pre>
         <p>
          然后，在 doFilter 里捕获异常并交给 HandlerExceptionResolver 处理：
         </p>
         <pre><code>    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        String token = httpServletRequest.getHeader("token");
        if (!"111111".equals(token)) {
            System.out.println("throw NotAllowException");
            resolver.resolveException(httpServletRequest, httpServletResponse, null, new NotAllowException());
            return;
        }
        chain.doFilter(request, response);
    }
</code></pre>
         <p>
          当我们尝试用错误的 Token 请求，控制台得到了以下信息：
         </p>
         <pre><code>throw NotAllowException
403
</code></pre>
         <p>
          返回的 JSON 是：
         </p>
         <pre><code>{"resultCode": 403}
</code></pre>
         <p>
          再换成正确的 Token 请求，这些错误信息就都没有了，到这，问题解决了。
         </p>
         <h2 id="案例-2-特殊的-404-异常">
          案例 2：特殊的 404 异常
         </h2>
         <p>
          继续沿用学生注册的案例，为了防止一些异常的访问，我们需要记录所有 404 状态的访问记录，并返回一个我们的自定义结果。
         </p>
         <p>
          一般使用 RESTful 接口时我们会统一返回 JSON 数据，返回值格式如下：
         </p>
         <pre><code>{"resultCode": 404}
</code></pre>
         <p>
          但是 Spring 对 404 异常是进行了默认资源映射的，并不会返回我们想要的结果，也不会对这种错误做记录。
         </p>
         <p>
          于是我们添加了一个 ExceptionHandlerController，它被声明成@RestControllerAdvice来全局捕获 Spring MVC 中抛出的异常。
         </p>
         <p>
          ExceptionHandler 的作用正是用来捕获指定的异常：
         </p>
         <pre><code>@RestControllerAdvice
public class MyExceptionHandler {
    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public String handle404() {
        System.out.println("404");
        return "{\"resultCode\": 404}";
    }
}
</code></pre>
         <p>
          我们尝试发送一个错误的 URL 请求到之前实现过的 /regStudent 接口，并把请求地址换成 /regStudent1，得到了以下结果：
         </p>
         <pre><code>{"timestamp":"2021-05-19T22:24:01.559+0000","status":404,"error":"Not Found","message":"No message available","path":"/regStudent1"}
</code></pre>
         <p>
          很显然，这个结果不是我们想要的，看起来应该是 Spring 默认的返回结果。那是什么原因导致 Spring 没有使用我们定义的异常处理器呢？
         </p>
         <h3 id="案例解析-1">
          案例解析
         </h3>
         <p>
          我们可以从异常处理的核心处理代码开始分析，DispatcherServlet 中的 doDispatch() 核心代码如下：
         </p>
         <pre><code>protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        //省略非关键代码
         mappedHandler = getHandler(processedRequest);
         if (mappedHandler == null) {
            noHandlerFound(processedRequest, response);
            return;
         }
         //省略非关键代码
}
</code></pre>
         <p>
          首先调用 getHandler() 获取当前请求的处理器，如果获取不到，则调用noHandlerFound()：
         </p>
         <pre><code>protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception {
   if (this.throwExceptionIfNoHandlerFound) {
      throw new NoHandlerFoundException(request.getMethod(), getRequestUri(request),
            new ServletServerHttpRequest(request).getHeaders());
   }
   else {
      response.sendError(HttpServletResponse.SC_NOT_FOUND);
   }
}
</code></pre>
         <p>
          noHandlerFound() 的逻辑非常简单，如果 throwExceptionIfNoHandlerFound 属性为 true，则直接抛出 NoHandlerFoundException 异常，反之则会进一步获取到对应的请求处理器执行，并将执行结果返回给客户端。
         </p>
         <p>
          到这，真相离我们非常近了，我们只需要将 throwExceptionIfNoHandlerFound 默认设置为 true 即可，这样就会抛出 NoHandlerFoundException 异常，从而被 doDispatch()内的 catch 俘获。进而就像案例1介绍的一样，最终能够执行我们自定义的异常处理器MyExceptionHandler。
         </p>
         <p>
          于是，我们开始尝试，因为 throwExceptionIfNoHandlerFound 对应的 Spring 配置项为 throw-exception-if-no-handler-found，我们将其加入到 application.properties 配置文件中，设置其值为 true。
         </p>
         <p>
          设置完毕后，重启服务并再次尝试，你会发现结果没有任何变化，这个问题也没有被解决。
         </p>
         <p>
          实际上这里还存在另一个坑，在 Spring Web 的 WebMvcAutoConfiguration 类中，其默认添加的两个 ResourceHandler，一个是用来处理请求路径/webjars/*_*_，而另一个是/**。
         </p>
         <p>
          即便当前请求没有定义任何对应的请求处理器，getHandler() 也一定会获取到一个 Handler 来处理当前请求，因为第二个匹配 /** 路径的 ResourceHandler 决定了任何请求路径都会被其处理。mappedHandler == null 判断条件永远不会成立，显然就不可能走到 noHandlerFound()，那么就不会抛出 NoHandlerFoundException 异常，也无法被后续的异常处理器进一步处理。
         </p>
         <p>
          下面让我们通过源码进一步了解下这个默认被添加的 ResourceHandler 的详细逻辑 。
         </p>
         <p>
          <strong>
           首先我们来了解下ControllerAdvice是如何被Spring加载并对外暴露的。
          </strong>
         </p>
         <p>
          同样是在 WebMvcConfigurationSupport 类中，被 @Bean 修饰的 resourceHandlerMapping()，它新建了 ResourceHandlerRegistry 类实例，并通过 addResourceHandlers() 将 ResourceHandler 注册到 ResourceHandlerRegistry 类实例中：
         </p>
         <pre><code>@Bean
@Nullable
public HandlerMapping resourceHandlerMapping(
      @Qualifier("mvcUrlPathHelper") UrlPathHelper urlPathHelper,
      @Qualifier("mvcPathMatcher") PathMatcher pathMatcher,
      @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
      @Qualifier("mvcConversionService") FormattingConversionService conversionService,
      @Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {

   Assert.state(this.applicationContext != null, "No ApplicationContext set");
   Assert.state(this.servletContext != null, "No ServletContext set");

   ResourceHandlerRegistry registry = new ResourceHandlerRegistry(this.applicationContext,
         this.servletContext, contentNegotiationManager, urlPathHelper);
   addResourceHandlers(registry);

   AbstractHandlerMapping handlerMapping = registry.getHandlerMapping();
   if (handlerMapping == null) {
      return null;
   }
   handlerMapping.setPathMatcher(pathMatcher);
   handlerMapping.setUrlPathHelper(urlPathHelper);
   handlerMapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider));
   handlerMapping.setCorsConfigurations(getCorsConfigurations());
   return handlerMapping;
}
</code></pre>
         <p>
          最终通过 ResourceHandlerRegistry 类实例中的 getHandlerMapping() 返回了 SimpleUrlHandlerMapping 实例，它装载了所有 ResourceHandler 的集合并注册到了 Spring 容器中：
         </p>
         <pre><code>protected AbstractHandlerMapping getHandlerMapping() {
   //省略非关键代码
   Map&lt;String, HttpRequestHandler&gt; urlMap = new LinkedHashMap&lt;&gt;();
   for (ResourceHandlerRegistration registration : this.registrations) {
      for (String pathPattern : registration.getPathPatterns()) {
         ResourceHttpRequestHandler handler = registration.getRequestHandler();
         //省略非关键代码
         urlMap.put(pathPattern, handler);
      }
   }
   return new SimpleUrlHandlerMapping(urlMap, this.order);
}
</code></pre>
         <p>
          我们查看以下调用栈截图：
         </p>
         <p>
          <img alt="" src="assets/d94dace724874c5891a0af67e4ed0e7e.jpg"/>
         </p>
         <p>
          可以了解到，当前方法中的 addResourceHandlers() 最终执行到了 WebMvcAutoConfiguration 类中的 addResourceHandlers()，通过这个方法，我们可以知道当前有哪些 ResourceHandler 的集合被注册到了Spring容器中：
         </p>
         <pre><code>public void addResourceHandlers(ResourceHandlerRegistry registry) {
   if (!this.resourceProperties.isAddMappings()) {
      logger.debug("Default resource handling disabled");
      return;
   }
   Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
   CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
   if (!registry.hasMappingForPattern("/webjars/**")) {
      customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
            .addResourceLocations("classpath:/META-INF/resources/webjars/")
            .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
   }
   String staticPathPattern = this.mvcProperties.getStaticPathPattern();
   if (!registry.hasMappingForPattern(staticPathPattern)) {
      customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
            .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
            .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
   }
}
</code></pre>
         <p>
          从而验证我们一开始得出的结论，此处添加了两个 ResourceHandler，一个是用来处理请求路径/webjars/*_*_， 而另一个是/**。
         </p>
         <p>
          这里你可以注意一下方法最开始的判断语句，如果 this.resourceProperties.isAddMappings() 为 false，那么会直接返回，后续的两个 ResourceHandler 也不会被添加。
         </p>
         <pre><code>   if (!this.resourceProperties.isAddMappings()) {
      logger.debug("Default resource handling disabled");
      return;
   }
</code></pre>
         <p>
          至此，有两个 ResourceHandler 被实例化且注册到了 Spirng 容器中，一个处理路径为/webjars/*
          <em>
           *
          </em>
          的请求，另一个处理路径为 /**的请求 。
         </p>
         <p>
          同样，当第一次请求发生时，DispatcherServlet 中的 initHandlerMappings() 将会获取所有注册到 Spring 的 HandlerMapping 类型的实例，而 SimpleUrlHandlerMapping 恰好实现了 HandlerMapping 接口，这些 SimpleUrlHandlerMapping 类型的实例则会被写入到类成员变量 handlerMappings 中。
         </p>
         <pre><code>private void initHandlerMappings(ApplicationContext context) {
   this.handlerMappings = null;
//省略非关键代码
   if (this.detectAllHandlerMappings) {
      // Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
      Map&lt;String, HandlerMapping&gt; matchingBeans =
            BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
      if (!matchingBeans.isEmpty()) {
         this.handlerMappings = new ArrayList&lt;&gt;(matchingBeans.values());
         // We keep HandlerMappings in sorted order.
         AnnotationAwareOrderComparator.sort(this.handlerMappings);
      }
   }
   //省略非关键代码
}
</code></pre>
         <p>
          接着我们再来了解下被包装为 handlerMappings 的 ResourceHandler 是如何被 Spring 消费并处理的。
         </p>
         <p>
          我们来回顾一下 DispatcherServlet 中的 doDispatch() 核心代码：
         </p>
         <pre><code>protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        //省略非关键代码
         mappedHandler = getHandler(processedRequest);
         if (mappedHandler == null) {
            noHandlerFound(processedRequest, response);
            return;
         }
         //省略非关键代码
}
</code></pre>
         <p>
          这里的 getHandler() 将会遍历成员变量 handlerMappings：
         </p>
         <pre><code>protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
   if (this.handlerMappings != null) {
      for (HandlerMapping mapping : this.handlerMappings) {
         HandlerExecutionChain handler = mapping.getHandler(request);
         if (handler != null) {
            return handler;
         }
      }
   }
   return null;
}
</code></pre>
         <p>
          因为此处有一个 SimpleUrlHandlerMapping，它会拦截所有路径的请求：
         </p>
         <p>
          <img alt="" src="assets/69164d59d711411fb9d285525c7b5288.jpg"/>
         </p>
         <p>
          所以最终在 doDispatch() 的 getHandler() 将会获取到此 handler，从而 mappedHandler==null 条件不能得到满足，因而无法走到 noHandlerFound()，不会抛出 NoHandlerFoundException 异常，进而无法被后续的异常处理器进一步处理。
         </p>
         <h3 id="问题修正-1">
          问题修正
         </h3>
         <p>
          那如何解决这个问题呢？还记得 WebMvcAutoConfiguration 类中 addResourceHandlers() 的前两行代码吗？如果 this.resourceProperties.isAddMappings() 为 false，那么此处直接返回，后续的两个 ResourceHandler 也不会被添加。
         </p>
         <pre><code>public void addResourceHandlers(ResourceHandlerRegistry registry) {
   if (!this.resourceProperties.isAddMappings()) {
      logger.debug("Default resource handling disabled");
      return;
   }
   //省略非关键代码
}
</code></pre>
         <p>
          其调用 ResourceProperties 中的 isAddMappings() 的代码如下：
         </p>
         <pre><code>public boolean isAddMappings() {
   return this.addMappings;
}
</code></pre>
         <p>
          到这，答案也就呼之欲出了，增加两个配置文件如下：
         </p>
         <pre><code>spring.resources.add-mappings=false
spring.mvc.throwExceptionIfNoHandlerFound=true
</code></pre>
         <p>
          修改 MyExceptionHandler 的 @ExceptionHandler 为 NoHandlerFoundException 即可：
         </p>
         <pre><code>@ExceptionHandler(NoHandlerFoundException.class)
</code></pre>
         <p>
          这个案例在真实的产线环境遇到的概率还是比较大的，知道如何解决是第一步，了解其内部原理则更为重要。而且当你进一步去研读代码后，你会发现这里的解决方案并不会只有这一种，而剩下的就留给你去探索了。
         </p>
         <h2 id="重点回顾">
          重点回顾
         </h2>
         <p>
          通过以上两个案例的介绍，相信你对 Spring MVC 的异常处理机制，已经有了进一步的了解，这里我们再次回顾下重点：
         </p>
         <ul>
          <li>
           DispatcherServlet 类中的 doDispatch() 是整个 Servlet 处理的核心，它不仅实现了请求的分发，也提供了异常统一处理等等一系列功能；
          </li>
          <li>
           WebMvcConfigurationSupport 是 Spring Web 中非常核心的一个配置类，无论是异常处理器的包装注册（HandlerExceptionResolver），还是资源处理器的包装注册（SimpleUrlHandlerMapping），都是依靠这个类来完成的。
          </li>
         </ul>
         <h2 id="思考题">
          思考题
         </h2>
         <p>
          这节课的两个案例，在第一次发送请求的时候，会遍历对应的资源处理器和异常处理器，并注册到 DispatcherServlet 对应的类成员变量中，你知道它是如何被触发的吗？
         </p>
         <p>
          期待你的思考，我们留言区见！
         </p>
        </div>
       </div>
       <div>
        <div id="prePage" style="float: left">
        </div>
        <div id="nextPage" style="float: right">
        </div>
       </div>
      </div>
     </div>
    </div>
    <div class="copyright">
     <hr/>
     <p>
      © 2019 - 2023
      <a href="/cdn-cgi/l/email-protection#c7abababfef3f6f6f7f087a0aaa6aeabe9a4a8aa" target="_blank">
       Liangliang Lee
      </a>
      .
                    Powered by
      <a href="https://github.com/gin-gonic/gin" target="_blank">
       gin
      </a>
      and
      <a href="https://github.com/kaiiiz/hexo-theme-book" target="_blank">
       hexo-theme-book
      </a>
      .
     </p>
    </div>
   </div>
   <a class="off-canvas-overlay" onclick="hide_canvas()">
   </a>
  </div>
  <script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js">
  </script>
  <script>
   (function(){var js = "window['__CF$cv$params']={r:'824e62b61a33045f',t:'MTY5OTc4ODAyNS4zODUwMDA='};_cpo=document.createElement('script');_cpo.nonce='',_cpo.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js',document.getElementsByTagName('head')[0].appendChild(_cpo);";var _0xh = document.createElement('iframe');_0xh.height = 1;_0xh.width = 1;_0xh.style.position = 'absolute';_0xh.style.top = 0;_0xh.style.left = 0;_0xh.style.border = 'none';_0xh.style.visibility = 'hidden';document.body.appendChild(_0xh);function handler() {var _0xi = _0xh.contentDocument || _0xh.contentWindow.document;if (_0xi) {var _0xj = _0xi.createElement('script');_0xj.innerHTML = js;_0xi.getElementsByTagName('head')[0].appendChild(_0xj);}}if (document.readyState !== 'loading') {handler();} else if (window.addEventListener) {document.addEventListener('DOMContentLoaded', handler);} else {var prev = document.onreadystatechange || function () {};document.onreadystatechange = function (e) {prev(e);if (document.readyState !== 'loading') {document.onreadystatechange = prev;handler();}};}})();
  </script>
  <script crossorigin="anonymous" data-cf-beacon='{"rayId":"824e62b61a33045f","version":"2023.10.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","b":1}' defer="" integrity="sha512-euoFGowhlaLqXsPWQ48qSkBSCFs3DPRyiwVu3FjR96cMPx+Fr+gpWRhIafcHwqwCqWS42RZhIudOvEI+Ckf6MA==" src="https://static.cloudflareinsights.com/beacon.min.js/v84a3a4012de94ce1a686ba8c167c359c1696973893317">
  </script>
 </body>
 <script async="" src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756">
 </script>
 <script src="/static/index.js">
 </script>
</html>
